<template>
  <view class="game-container">
    <canvas class="game-canvas" canvas-id="canvas" />
    <view v-if="result" class="game-over">
      <view class="title">游戏已结束</view>
      <view class="points">得分: {{ points }}</view>
      <view class="buttons">
        <view class="replay-button" @click="startGame" />
        <view v-if="result === 'sucuess'" class="next-button" />
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { onMounted, ref, watch, watchEffect } from 'vue'
import { bgImg, bgPlaneImg, bulletImg, channelKeys, explosionFrams, itemBgImg, keys, loadedCount, requireCount, rocketImg, rocketSmallImg } from './assets'
import bgMusic from '@/static/airplane/audit/game-bgmusic.mp3'
import shootSound from '@/static/airplane/audit/shoot.mp3'
import explosionSound from '@/static/airplane/audit/enemy_down.mp3'

// 生成间隔
const spawnIntervals = 1500
// 下降速度
const itemSpeed = 0.1
// 字母背景大小
const itemSize = 50
// 字母颜色
const itemColor = 'white'
// 字母字体大小
const itemFontSize = 32
// 左右间隔
const itemSpacing = 50

// 爆炸时间
const explosionTime = 500
// 爆炸大小
const explosionHeight = 128
// 子弹大小
const bulletHeight = 60
// 子弹速度
const bulletSpead = 0.5

// 最大血量
const maxHealth = 10
// 每次扣多少血
const danageHealth = 1

// 背景滚动速度
const bgSpeed = 0.1
// 背景上的球滚动速度
const bgPlanetSpeed = 0.2

// 飞船离底部的距离
const rockBotton = 100
// 大飞船的高度
const rockHeight = 80
// 小飞船的高度
const smallRockHeight = 40

let width: number
let height: number
let ctx: UniApp.CanvasContext

// data
const result = ref<'sucuess' | 'fail'>()

let game = false

watch(result, () => {
  if (result.value) {
    game = false
  }
})

let health = 0

let points = 0

const channels: Channel[] = []

let currentIndex = 0

let lastRenderTime = 0

let nextSpawnTime = 0

let isMounted = false
onMounted(() => {
  uni.showLoading()
  isMounted = true
  if (loadedCount.value === requireCount.value) {
    onReady()
  }
})
watchEffect(() => {
  if (ctx) {
    return
  }
  if (loadedCount.value === requireCount.value) {
    if (isMounted) {
      onReady()
    }
  }
})

function onReady() {
  uni.hideLoading()
  width = document.body.clientWidth
  height = document.body.clientHeight

  function fillChanelData(left: number) {
    for (let i = 0, j = 4; i < j; i++) {
      const offset = i * itemSize + i * itemSpacing

      const center = left + offset + itemSize / 2
      channels.push({
        center,
        itemLeft: center - itemSize / 2,
        items: [],
        bullets: [],
        explosions: [],
      })
    }
  }

  const space = 4 * itemSize + 3 * itemSpacing
  fillChanelData(width * 0.25 - space / 2)
  fillChanelData(width * 0.75 - space / 2)

  const muscic = uni.createInnerAudioContext()
  muscic.src = bgMusic
	
  muscic.autoplay = true
  muscic.loop = true

  ctx = uni.createCanvasContext('canvas')

  requestAnimationFrame(render)

  document.onkeydown = (e: KeyboardEvent) => {
    if (e.key === ' ') {
      if (!result.value && !game) {
        startGame()
      }
      return
    }
    if (!game) {
      return
    }
    const index = channelKeys.findIndex(key => key.includes(e.key))
    if (index !== -1) {
      const channel = channels[index]
      const item = channel.items[channel.bullets.length]
      if (item && item.key === e.key) {
        channel.bullets.push({
          key: e.key,
          y: height - rockBotton - smallRockHeight,
        })
      }
      else {
        danage()
      }
      sound(shootSound)
    }
  }
}

function render(date: number) {
  requestAnimationFrame(render)

  renderBG(date)
  renderBGPlanet(date)
  if (!game) {
    if (!result.value) {
      ctx.fillStyle = 'white'
      ctx.setFontSize(80)

      fillText('按空格开始', 300)
    }
  }
  else {
    renderGame(date)
    renderBasic()
    renderExplostion(date)

    checkIsSucuess()

    ctx.fillStyle = 'red'
    ctx.fillRect(30, 30, (health / maxHealth) * 140, 20)
    ctx.strokeStyle = 'white'
    ctx.strokeRect(30, 30, 140, 20)
  }

  ctx.draw()

  lastRenderTime = date
}

function startGame() {
  result.value = undefined
  game = true
  health = maxHealth
  channels.forEach((channel) => {
    channel.items = []
    channel.bullets = []
    channel.explosions = []
  })
}

function checkIsSucuess() {
  if (currentIndex === keys.length) {
    const hasItem = channels.find(channel => !(channel.items.length === 0 && channel.explosions.length === 0))
    if (!hasItem) {
      result.value = 'sucuess'
    }
  }
}

// ========== backgroud ==========
let bgLocation = 0

function renderBG(time: number) {
  const zoom = width / bgImg.naturalWidth

  const renderHegiht = bgImg.naturalHeight * zoom

  ctx.drawImage(bgImg.src, 0, (renderHegiht - height - bgLocation) / zoom, bgImg.naturalWidth, height / zoom, 0, 0, width, height)

  const freeSpace = height - (renderHegiht - bgLocation)

  if (freeSpace > 0) {
    ctx.drawImage(bgImg.src, 0, (renderHegiht - freeSpace) / zoom, bgImg.naturalWidth, freeSpace / zoom, 0, 0, width, freeSpace)
  }
  bgLocation += (time - lastRenderTime) * bgSpeed
  bgLocation %= renderHegiht
}

let bgPlanetLocation = 0

function renderBGPlanet(time: number) {
  const zoom = width / bgPlaneImg.naturalWidth

  const renderHegiht = bgPlaneImg.naturalHeight * zoom

  ctx.drawImage(bgPlaneImg.src, 0, (renderHegiht - height - bgPlanetLocation) / zoom, bgPlaneImg.naturalWidth, height / zoom, 0, 0, width, height)

  const freeSpace = height - (renderHegiht - bgPlanetLocation)

  if (freeSpace > 0) {
    ctx.drawImage(bgPlaneImg.src, 0, (renderHegiht - freeSpace) / zoom, bgPlaneImg.naturalWidth, freeSpace / zoom, 0, 0, width, freeSpace)
  }
  bgPlanetLocation += (time - lastRenderTime) * bgPlanetSpeed
  bgPlanetLocation %= bgPlaneImg.naturalHeight * zoom
}

// ========== basic ==========
function renderBasic() {
  for (const channel of channels) {
    renderImage(rocketSmallImg, height - rockBotton - smallRockHeight, channel.center, smallRockHeight)
  }

  renderImage(rocketImg, height - rockBotton - rockHeight, width / 2, rockHeight)
}

// ========== game ==========
function danage() {
  health = Math.max(health - danageHealth, 0)

  if (health === 0) {
    result.value = 'fail'
  }
}

function renderGame(time: number) {
  if (nextSpawnTime < time && currentIndex !== keys.length) {
    const key = keys[currentIndex++]

    const channelId = channelKeys.findIndex(it => it.includes(key))

    channels[channelId].items.push({
      key,
      y: 0,
    })

    nextSpawnTime = time + spawnIntervals
  }

  const interval = time - lastRenderTime

  for (const channel of channels) {
    const { items, bullets, explosions } = channel

    items.forEach(item => item.y += interval * itemSpeed)
    bullets.forEach(bullet => bullet.y -= interval * bulletSpead)

    const rockTop = height - rockBotton - smallRockHeight

    for (const item of items) {
      if (item.y > rockTop - itemSize) {
        explosions.push({
          time,
          y: rockTop - explosionFrams[0].naturalHeight,
        })
        danage()
        sound(explosionSound)
        items.shift()

        continue
      }
      break
    }

    for (const bullet of bullets) {
      if (bullet.y < 0) {
        bullets.shift()
        explosions.push({
          time,
          y: bullet.y,
        })
        danage()
        sound(explosionSound)
      }
    }

    for (const bullet of bullets) {
      const fisrtItem = items[0]

      if (!fisrtItem) {
        break
      }

      if (fisrtItem.y + itemSize > bullet.y) {
        if (fisrtItem.key !== bullet.key) {
          bullets.shift()
          danage()
          sound(explosionSound)
        }
        else {
          bullets.shift()
          items.shift()
          points++
          sound(explosionSound)
        }

        explosions.push({
          time,
          y: bullet.y + (bullet.y - fisrtItem.y) / 2 - explosionFrams[0].naturalHeight / 2,
        })
        continue
      }
      break
    }

    // 渲染部分
    for (const item of items) {
      renderImage(itemBgImg, item.y, channel.center, itemSize)

      const text = item.key.toLocaleUpperCase()

      const textSize = ctx.measureText(text)

      const left = channel.itemLeft + itemSize / 2 - textSize.width / 2
      const top = item.y + itemSize / 2 + itemFontSize / 2
      ctx.setFontSize(itemFontSize)
      ctx.fillStyle = itemColor
      ctx.fillText(text, left, top)
    }

    for (const bullet of bullets) {
      renderImage(bulletImg, bullet.y, channel.center, bulletHeight)
    }
  }
}

// ========== explostion ==========
function renderExplostion(time: number) {
  for (const channel of channels) {
    channel.explosions.forEach((explosion, index) => {
      const range = explosionTime / explosionFrams.length
      const liveTime = time - explosion.time

      const fram = Math.floor(liveTime / range)

      if (fram >= explosionFrams.length) {
        channel.explosions.splice(index, 1)
        return
      }

      renderImage(explosionFrams[fram], explosion.y, channel.center, explosionHeight)
    })
  }
}

// ========== utils ==========
function random(max: number) {
  return Math.floor(Math.random() * max)
}

function randomA<T>(list: T[]) {
  return list[random(list.length)]
}

function renderImage(image: HTMLImageElement, top: number, center: number, height: number) {
  const zoom = height / image.naturalHeight

  const width = zoom * image.naturalWidth

  const left = center - width / 2

  ctx.drawImage(image.src, left, top, width, height)
}

function sound(src: string) {
  const audit = uni.createInnerAudioContext()
  audit.src = src
  audit.play()
}

function fillText(text: string, top: number) {
  const textSize = ctx.measureText(text)

  ctx.fillText(text, width / 2 - textSize.width / 2, top)
}

function clt(center: number, width: number) {
  return center - width / 2
}

// ========== interfaces ==========
interface ExplostionData {
  time: number
  y: number
}

interface Item {
  key: string
  y: number
}

interface Channel {
  center: number
  itemLeft: number
  items: Item[]
  bullets: Item[]
  explosions: ExplostionData[]
}
</script>

<style lang="scss">
page {
  height: 100%;
}

.game-container {
  width: 100%;
  height: 100%;
}

.game-canvas {
  width: 100%;
  height: 100%;
}

.game-over {
  position: absolute;
  top: 200px;
  width: 100%;
  color: white;
  text-align: center;
  background-size: cover;

  .title {
    font-size: 80px;
  }

  .points {
    margin-top: 30px;
    font-size: 32px;
  }

  .buttons {
    display: flex;
    gap: 50px;
    justify-content: center;
    margin-top: 50px;
  }

  .replay-button {
    width: 170px;
    height: 50px;
    background-size: cover;
    background-image: url("@/static/airplane/button/game-btn-again.png");

    &:hover {
      background-image: url("@/static/airplane/button/game-btn-again-hover.png");
    }
  }

  .next-button {
    width: 170px;
    height: 50px;
    background-size: cover;
    background-image: url("@/static/airplane/button/game-btn-continue.png");

    &:hover {
      background-image: url("@/static/airplane/button/game-btn-continue-hover.png");
    }
  }
}
</style>
